module BBCode
  
  module REGEXP
    
    module MAKE
      
      def attrs(attrs)
        attr_regexp = "";
        attrs.each do |name, value|
          _attr = attr(:attr_pair, :name => name, :value => value) + "\\s*";
          attr_regexp << _attr;
        end
        Regexp.new(attr_regexp);        
      end
      
      def attr(attr_type, opts)
        attr = self.const_get(attr_type.to_s.upcase).dup;
        substr = lambda{|const, opt| attr.gsub!(const, opts[opt].to_s) if opts[opt]}
        substr.call(self::ATTR_NAME_STRING+"+", :name);
        substr.call(self::ATTR_VALUE_STRING+"+", :value);
        substr.call(self::QUOTES, :quotes);
        Regexp.new(attr);
      end
      
      def tag(tag_type, opts)
        tag = self.const_get(tag_type.to_s.upcase).dup;
        tag = tag.gsub(self::TAG_NAME+"+", opts[:name]) if opts[:name];
        attrs = (opts[:attrs].kind_of?(Hash)) ? attrs(opts[:attrs]) : opts[:attrs];
        tag = tag.gsub(self::TAG_STRING+"*", attrs) if attrs;
        Regexp.new(tag);
      end
      
    end
    
    module HTML

      #ATTR_NAME_STRING = "[a-zA-Z]";
      ATTR_NAME_STRING = "[a-z]";
      
      ATTR_VALUE_STRING = "[^\"'>]";
      
      QUOTES = "(?:\"|')";
      
      C_ATTR_NAME = "(#{ATTR_NAME_STRING}+)";
      
      # Match an attribute name (Ex. color)
      # 
      ATTR_NAME = "#{C_ATTR_NAME}=";
      
      C_ATTR_VALUE = "#{QUOTES}(#{ATTR_VALUE_STRING}+)#{QUOTES}";
      
      # Match an attribute value (Ex. blue)
      # 
      ATTR_VALUE = "=#{C_ATTR_VALUE}";
      
      # Match an attribute pair (Ex. color="blue" or color='blue')
      #
      ATTR_PAIR = "#{C_ATTR_NAME}=#{C_ATTR_VALUE}";
      ATTR = ATTR_PAIR;      
      
      TAG_NAME = "[^\"'>/\\s=]";
      TAG_STRING = "[^>]";
      TAG_INNER = "(.*?)"
      
      C_OPEN = "(#{TAG_NAME}+)\\s*(#{TAG_STRING}*?)\\s*";
      
      # Match open tags in the text (Ex. <car color="blue">)
      #
      OPEN_TAG = "<#{C_OPEN}>"
      
      C_CLOSE = "/(#{TAG_NAME}+)";
      
      # Match close tags in the text (Ex. </car>)
      #
      CLOSE_TAG = "<#{C_CLOSE}>";
      
      # Match single tags in the text (Ex. <car color="blue"/>)
      #
      SINGLE_TAG = "<#{C_OPEN}/>";
      SINGLE_NODE = SINGLE_TAG;
      
      # Match full tags in the text (Ex. <car color="blue">Ferrari</car>), 
      # including the content
      #
      FULL_TAG = "#{OPEN_TAG}(#{TAG_INNER}*)#{CLOSE_TAG}";
      FULL_NODE = FULL_TAG;
      
      # Match simple tags in the text (Ex. <car> or </car> or <car/>),
      # without attributes
      #
      SIMPLE_TAG = "<(/?)(#{TAG_NAME}+)(/?)>"

      # Match a generic tag, can be open, close, single, whatever. But doesn't 
      # match full nodes or tags, just use with open tags that has attributes
      #
      GENERIC_TAG = "<(/?)#{C_OPEN}(/?)>";
      TAG = GENERIC_TAG;
      
      extend BBCode::REGEXP::MAKE;
      
      ENTITIES = {251 => "&ucirc;", 232 => "&egrave;", 177 => "&plusmn;", 195 => "&Atilde;", 214 => "&Ouml;", 252 => "&uuml;", 233 => "&eacute;", 196 => "&Auml;", 38 => "&amp;", 215 => "&times;", 178 => "&sup2;", 253 => "&yacute;", 234 => "&ecirc;", 197 => "&Aring;", 216 => "&Oslash;", 179 => "&sup3;", 254 => "&thorn;", 235 => "&euml;", 198 => "&AElig;", 217 => "&Ugrave;", 161 => "&iexcl;", 180 => "&acute;", 60 => "&lt;", 255 => "&yuml;", 236 => "&igrave;", 218 => "&Uacute;", 162 => "&cent;", 181 => "&micro;", 199 => "&Ccedil;", 237 => "&iacute;", 219 => "&Ucirc;", 163 => "&pound;", 182 => "&para;", 200 => "&Egrave;", 62 => "&gt;", 238 => "&icirc;", 220 => "&Uuml;", 164 => "&curren;", 183 => "&middot;", 201 => "&Eacute;", 239 => "&iuml;", 165 => "&yen;", 184 => "&cedil;", 202 => "&Ecirc;", 221 => "&Yacute;", 240 => "&eth;", 166 => "&brvbar;", 185 => "&sup1;", 203 => "&Euml;", 222 => "&THORN;", 241 => "&ntilde;", 186 => "&ordm;", 204 => "&Igrave;", 223 => "&szlig;", 167 => "&sect;", 242 => "&ograve;", 187 => "&raquo;", 205 => "&Iacute;", 168 => "&uml;", 243 => "&oacute;", 188 => "&frac14;", 206 => "&Icirc;", 224 => "&agrave;", 169 => "&copy;", 244 => "&ocirc;", 225 => "&aacute;", 207 => "&Iuml;", 170 => "&ordf;", 189 => "&frac12;", 245 => "&otilde;", 226 => "&acirc;", 208 => "&ETH;", 171 => "&laquo;", 190 => "&frac34;", 246 => "&ouml;", 227 => "&atilde;", 209 => "&Ntilde;", 172 => "&not;", 191 => "&iquest;", 32 => "&nbsp;", 247 => "&divide;", 228 => "&auml;", 173 => "&shy;", 210 => "&Ograve;", 34 => "&quot;", 248 => "&oslash;", 229 => "&aring;", 174 => "&reg;", 192 => "&Agrave;", 211 => "&Oacute;", 249 => "&ugrave;", 230 => "&aelig;", 175 => "&macr;", 193 => "&Aacute;", 212 => "&Ocirc;", 250 => "&uacute;", 231 => "&ccedil;", 176 => "&deg;", 194 => "&Acirc;", 213 => "&Otilde;"};
      
    end
    
    module BBCODE
      
      html = BBCode::REGEXP::HTML
      html.constants.each do |const_name|
        const_value = html.const_get(const_name);
        if const_value.kind_of?(String)
          const_value = const_value.gsub("<", "\\[").gsub(">", "\\]");
          self.const_set(const_name, const_value);
        end
      end
      
      TAG_ATTR = "\\[(#{TAG_NAME}+)=#{QUOTES}?(#{ATTR_VALUE_STRING}+)#{QUOTES}?\d*/?\\]";
      FULL_TAG_ATTR = "#{TAG_ATTR}#{TAG_INNER}#{CLOSE_TAG}";
      FULL_NODE_ATTR = FULL_TAG_ATTR;
      
      extend BBCode::REGEXP::MAKE;
      
    end
    
  end
  
  # Make an scan in the text and replace *bb_tags* with *replace_text*, the latter can have '\1', '\2' as groups.
  # There are 4 groups as seen in the example below:
  #
  # text = "[url='www.gooogle.com']Google[/url] is a big company, campus: [img='photo.jpg'/], [style color='']Real[/style]"
  # BBCode.replace_bbcode_tag(text, ["url", "img", "style"], "<(\\1)(\\2)(\\3)(\\4)>") #=> "<()(url)(='www.gooogle.com')()>Google<(/)(url)()()> is a big company, campus: <()(img)(='photo.jpg')(/)>, <()(style)(color='')()>Real<(/)(style)()()>"
  #
  def self.replace_bbcode_tag(text, bb_tags, replace_text, tag_type=:generic_tag)
    replace_tags(text, bb_tags, replace_text){|tag| BBCode::REGEXP::BBCODE.tag(tag_type, :name => tag)}
  end
  
  # Same as replace_bbcode_tag but with html tags
  #
  def self.replace_html_tag(text, html_tags, replace_text, tag_type=:generic_tag)
    replace_tags(text, bb_tags, replace_text){|tag| BBCode::REGEXP::HTML.tag(tag_type, :name => tag)}
  end
  
  def self.decode_entities(text)
    entities = BBCode::REGEXP::HTML::ENTITIES;
    entities.each do |code, entity|
      text = text.gsub(entity, code.chr);
    end
    text;
  end
  
  def self.encode_entities(text)
    entities = BBCode::REGEXP::HTML::ENTITIES;
    text = text.gsub(/[^\w ]/) do |str|
      (entities[str[0]]) ? entities[str[0]] : str;
    end
    text;
  end
  
  private
  def self.replace_tags(text, tags, replace)
    tags = [tags] unless tags.kind_of?(Array);
    replace = [replace] unless replace.kind_of?(Array);
    tags.each_with_index do |tag, i|
      regexp = yield(tag);
      text = text.gsub(regexp, (replace[i] || replace[0]));
    end
    text;
  end
  
end